package org.nicocube.airain.domain.server.data;

import java.util.Hashtable;
import java.util.Map;

import org.nicocube.airain.domain.client.data.OrderedStorable;
import org.nicocube.airain.domain.client.data.StorageException;
import org.nicocube.airain.ds.DSClient;
import org.nicocube.airain.utils.config.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.db4o.ObjectContainer;
import com.db4o.events.CancellableObjectEventArgs;
import com.db4o.events.Event4;
import com.db4o.events.EventArgs;
import com.db4o.events.EventListener4;
import com.db4o.events.EventRegistryFactory;
import com.db4o.events.ObjectEventArgs;

public class ContainerProviderImpl implements ContainerProvider {
	private static final Logger log = LoggerFactory.getLogger(ContainerProviderImpl.class); 
	
	private static ObjectContainer db;

	private Config config;
	
	private Map<Class<?>,EventHandler> eventCreating;
	private Map<Class<?>,EventHandler> eventUpdated;
	
	public static String DEFAULT_LOCAL_STORAGE_PROPERTIES = "./storage.properties";
	public static String DEFAULT_SYSTEM_STORAGE_PROPERTIES = "/etc/airain/storage.properties";
	
	/**
	 * constructor with injected Config
	 * @param config
	 */
	public ContainerProviderImpl(Config config) {
		this.config = config;
	}

	@Override
	public ObjectContainer getContainer() {
		try {
			return provideDB();
		} catch (StorageException e) {
			throw new IllegalStateException("",e);
		}
	}
	
	/**
	 * Provide a DB4O {@link ObjectContainer} we can use as DB 
	 * @return
	 * @throws StorageException
	 */
	private ObjectContainer provideDB() throws StorageException {
		if (db==null || db.ext().isClosed())
			importDB();
		return db;
	}
	
	private static class CommonListener implements EventListener4 {
		private Map<Class<?>,EventHandler> map;
		public CommonListener(Map<Class<?>,EventHandler> map) {
			this.map = map;
		}
		@Override
		public void onEvent(Event4 ev, EventArgs args) {
			log.debug("onEvent()");
			final ObjectEventArgs objectArgs = ((ObjectEventArgs) args);
			final Object obj = objectArgs.object();
			if (log.isDebugEnabled()) log.debug("onEvent():obj.getClass()="+obj.getClass());
			try {
				if (map.containsKey(obj.getClass()))
					map.get(obj.getClass()).handle(obj);
			} catch (CancelEventException e) {
				try {
					final CancellableObjectEventArgs cancellableArgs = ((CancellableObjectEventArgs) args);
					cancellableArgs.cancel();
				} catch (ClassCastException ignore) {
					// TODO: not cancellable -> how to treat :S
				}
			}
		}
	}
	
	/**
	 * Synchronized method to prevent multiple parallel {@link ObjectContainer} instanciation
	 * @throws StorageException
	 */
	private synchronized void importDB() throws StorageException {
		log.info("importing DB");
		if (db==null || db.ext().isClosed()) {
			db = DSClient.open(config);
			addEventOnDB();
		}
	}
	
	private void addEventOnDB() {
		if (eventCreating != null && eventCreating.size() > 0) {
			EventRegistryFactory.forObjectContainer(db).creating()
			.addListener(new CommonListener(eventCreating));
		}
		if (eventUpdated != null && eventUpdated.size() > 0) {
			EventRegistryFactory.forObjectContainer(db).updated()
			.addListener(new CommonListener(eventUpdated));
		}
	}

	@Override
	public void registerEvent(Class<? extends OrderedStorable> clazz,
			EventType type, EventHandler handler) throws StorageException {
		if (log.isDebugEnabled()) log.debug("registerEvent():clazz="+clazz+",type="+type);
		switch (type) {
		case creating:
			if (eventCreating == null) eventCreating = new Hashtable<Class<?>,EventHandler>();
			eventCreating.put(clazz, handler);
			break;
		case updated:
			if (eventUpdated == null) eventUpdated = new Hashtable<Class<?>,EventHandler>();
			eventUpdated.put(clazz, handler);
			break;
		default:
			break;
		}
		
		if (db!=null) addEventOnDB();
	}

}
